{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "initial_id",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:39.580119Z",
     "start_time": "2025-01-21T14:09:39.573580Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/usr/local/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
      "  from .autonotebook import tqdm as notebook_tqdm\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=10, micro=14, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 1.26.4\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.0\n",
      "torch 2.5.1+cu124\n",
      "cuda:0\n"
     ]
    }
   ],
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "\n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n",
    "\n",
    "seed = 42"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "1dad802b6500ab83",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.656263Z",
     "start_time": "2025-01-21T14:09:39.699901Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[(PosixPath('competitions/cifar-10/train/1.png'), 'frog'),\n",
      " (PosixPath('competitions/cifar-10/train/2.png'), 'truck'),\n",
      " (PosixPath('competitions/cifar-10/train/3.png'), 'truck'),\n",
      " (PosixPath('competitions/cifar-10/train/4.png'), 'deer'),\n",
      " (PosixPath('competitions/cifar-10/train/5.png'), 'automobile')]\n",
      "[(PosixPath('competitions/cifar-10/test/1.png'), 'cat'),\n",
      " (PosixPath('competitions/cifar-10/test/2.png'), 'cat'),\n",
      " (PosixPath('competitions/cifar-10/test/3.png'), 'cat'),\n",
      " (PosixPath('competitions/cifar-10/test/4.png'), 'cat'),\n",
      " (PosixPath('competitions/cifar-10/test/5.png'), 'cat')]\n",
      "50000 300000\n"
     ]
    }
   ],
   "source": [
    "from pathlib import Path\n",
    "\n",
    "# 目的：通过读取csv文件，得到图片的类别，并将图片路径和类别保存到DataFrame中。\n",
    "\n",
    "DATA_DIR1 = Path(\"./\")\n",
    "DATA_DIR2=Path(\"./competitions/cifar-10/\")\n",
    "\n",
    "train_labels_file = DATA_DIR1 / \"trainLabels.csv\"\n",
    "test_csv_file = DATA_DIR1 / \"sampleSubmission.csv\"  # 测试集模板csv文件\n",
    "train_folder = DATA_DIR2 / \"train/\"\n",
    "test_folder = DATA_DIR2 / \"test/\"\n",
    "\n",
    "# 所有的类别\n",
    "class_names = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']\n",
    "\n",
    "\n",
    "# filepath:csv文件路径，folder:图片所在文件夹\n",
    "def parse_csv_file(filepath, folder):\n",
    "    result = []\n",
    "    # 读取所有行\n",
    "    # with 语句：用于管理文件的上下文。在 with 块结束时，文件会自动关闭，无需手动调用 f.close()。\n",
    "    with open(filepath, 'r') as f:\n",
    "        # f.readlines()：读取文件的所有行，返回一个列表，每个元素是一行内容。\n",
    "        # 第一行不需要，因为第一行是标题\n",
    "        lines = f.readlines()[1:]\n",
    "    for line in lines:\n",
    "        # strip('\\n')：去除行末的换行符（\\n）。\n",
    "        # split(',')：按','分割字符串，返回一个列表。\n",
    "        image_id, label_str = line.strip('\\n').split(',')\n",
    "        # 得到图片的路径\n",
    "        image_full_path = folder / f\"{image_id}.png\"\n",
    "        # 得到对应图片的路径和分类\n",
    "        result.append((image_full_path, label_str))\n",
    "    return result\n",
    "\n",
    "\n",
    "# 得到训练集和测试集的图片路径和分类\n",
    "train_labels_info = parse_csv_file(train_labels_file, train_folder)\n",
    "test_csv_info = parse_csv_file(test_csv_file, test_folder)\n",
    "\n",
    "#打印\n",
    "import pprint\n",
    "\n",
    "pprint.pprint(train_labels_info[0:5])\n",
    "pprint.pprint(test_csv_info[0:5])\n",
    "print(len(train_labels_info), len(test_csv_info))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "8cd5063c39c678cf",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.732204Z",
     "start_time": "2025-01-21T14:09:41.658275Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "                            filepath       class\n",
      "0  competitions/cifar-10/train/1.png        frog\n",
      "1  competitions/cifar-10/train/2.png       truck\n",
      "2  competitions/cifar-10/train/3.png       truck\n",
      "3  competitions/cifar-10/train/4.png        deer\n",
      "4  competitions/cifar-10/train/5.png  automobile\n",
      "                                filepath       class\n",
      "0  competitions/cifar-10/train/45001.png       horse\n",
      "1  competitions/cifar-10/train/45002.png  automobile\n",
      "2  competitions/cifar-10/train/45003.png        deer\n",
      "3  competitions/cifar-10/train/45004.png  automobile\n",
      "4  competitions/cifar-10/train/45005.png    airplane\n",
      "                           filepath class\n",
      "0  competitions/cifar-10/test/1.png   cat\n",
      "1  competitions/cifar-10/test/2.png   cat\n",
      "2  competitions/cifar-10/test/3.png   cat\n",
      "3  competitions/cifar-10/test/4.png   cat\n",
      "4  competitions/cifar-10/test/5.png   cat\n"
     ]
    }
   ],
   "source": [
    "# 取前45000张图片作为训练集\n",
    "train_df = pd.DataFrame(train_labels_info[0:45000])\n",
    "# 取后5000张图片作为验证集\n",
    "valid_df = pd.DataFrame(train_labels_info[45000:])\n",
    "# 测试集\n",
    "test_df = pd.DataFrame(test_csv_info)\n",
    "\n",
    "# 为 Pandas DataFrame 的列重新命名\n",
    "train_df.columns = ['filepath', 'class']\n",
    "valid_df.columns = ['filepath', 'class']\n",
    "test_df.columns = ['filepath', 'class']\n",
    "\n",
    "print(train_df.head())\n",
    "print(valid_df.head())\n",
    "print(test_df.head())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "af9444393c2debca",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.740688Z",
     "start_time": "2025-01-21T14:09:41.732204Z"
    }
   },
   "outputs": [],
   "source": [
    "from PIL import Image\n",
    "from torch.utils.data import Dataset, DataLoader\n",
    "from torchvision import transforms\n",
    "\n",
    "\n",
    "class Cifar10Dataset(Dataset):\n",
    "    df_map = {\n",
    "        \"train\": train_df,\n",
    "        \"eval\": valid_df,\n",
    "        \"test\": test_df\n",
    "    }\n",
    "    # enumerate(class_names)：遍历 class_names，返回索引和类别名称的元组\n",
    "    # 将类别名称映射为索引，通常用于将标签转换为模型可以处理的数值。\n",
    "    label_to_idx = {\n",
    "        label: idx for idx, label in enumerate(class_names)\n",
    "    }\n",
    "    # 将索引映射为类别名称，通常用于将模型的输出（索引）转换回可读的类别名称\n",
    "    idx_to_label = {\n",
    "        idx: label for idx, label in enumerate(class_names)\n",
    "    }\n",
    "\n",
    "    def __init__(self, mode, transform=None):\n",
    "        # 获取对应模式的df，不同字符串对应不同模式\n",
    "        self.df = self.df_map.get(mode, None)\n",
    "        if self.df is None:\n",
    "            raise ValueError(f\"Invalid mode: {mode}\")\n",
    "        self.transform = transform\n",
    "\n",
    "    def __getitem__(self, index):\n",
    "        # 获取图片路径和标签\n",
    "        img_path, label = self.df.iloc[index]\n",
    "        # 打开一张图片并将其转换为 RGB 格式\n",
    "        img = Image.open(img_path).convert('RGB')  # 确保图片具有三个通道\n",
    "        # 应用数据增强\n",
    "        img = self.transform(img)\n",
    "        # label 转换为 idx\n",
    "        label = self.label_to_idx[label]\n",
    "        return img, label\n",
    "\n",
    "    def __len__(self):\n",
    "        # 返回df的行数,样本数\n",
    "        return self.df.shape[0]\n",
    "\n",
    "\n",
    "IMAGE_SIZE = 32\n",
    "mean, std = [0.4914, 0.4822, 0.4465], [0.247, 0.243, 0.261]\n",
    "\n",
    "#数据增强\n",
    "# ToTensor还将图像的维度从[height, width, channels]转换为[channels, height, width]。\n",
    "transforms_train = transforms.Compose([\n",
    "    transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),  # 缩放\n",
    "    transforms.RandomRotation(40),  # 随机旋转\n",
    "    transforms.RandomHorizontalFlip(),  # 随机水平翻转\n",
    "    transforms.ToTensor(),  # 转换为Tensor\n",
    "    transforms.Normalize(mean, std)  # 标准化\n",
    "])\n",
    "\n",
    "transforms_eval = transforms.Compose([\n",
    "    transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),\n",
    "    transforms.ToTensor(),\n",
    "    transforms.Normalize(mean, std)\n",
    "])\n",
    "\n",
    "train_ds = Cifar10Dataset(mode=\"train\", transform=transforms_train)\n",
    "eval_ds = Cifar10Dataset(mode=\"eval\", transform=transforms_eval)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "d17ef4a56f0b6ae1",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.748174Z",
     "start_time": "2025-01-21T14:09:41.742701Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "torch.Size([3, 32, 32])"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "train_ds[0][0].shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "161282a4e612afcd",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.753134Z",
     "start_time": "2025-01-21T14:09:41.749187Z"
    }
   },
   "outputs": [],
   "source": [
    "batch_size = 64\n",
    "\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size,\n",
    "                          shuffle=True)\n",
    "eval_loader = DataLoader(eval_ds, batch_size=batch_size,\n",
    "                         shuffle=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "20b5b31416450980",
   "metadata": {},
   "source": [
    "## 模型定义"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "b73daeeb83b4b527",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.760182Z",
     "start_time": "2025-01-21T14:09:41.754138Z"
    }
   },
   "outputs": [],
   "source": [
    "class Resdiual(nn.Module):\n",
    "    \"\"\"\n",
    "    浅层的残差块，无bottleneck（性能限制\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, input_channels, output_channels,\n",
    "                 use_1x1conv=False, stride=1):\n",
    "        \"\"\"\n",
    "        残差块\n",
    "        params filters: 过滤器数目，决定输出通道\n",
    "        params use_1x1conv: 是否使用 1x1 卷积，此时 stride=2，进行降采样\n",
    "        params strides: 步长，默认为1，当降采样的时候设置为2\n",
    "        \"\"\"\n",
    "        super().__init__()\n",
    "        # [1+(n+2-3)]//1=n \n",
    "        # [1+(n+2-3)]//2=n//2\n",
    "        # 即该方法可能会降采样\n",
    "        self.conv1 = nn.Conv2d(\n",
    "            in_channels=input_channels,\n",
    "            out_channels=output_channels,\n",
    "            kernel_size=3,\n",
    "            stride=stride,\n",
    "            padding=1\n",
    "        )\n",
    "        # [1+(n+2-3)]//1=n \n",
    "        self.conv2 = nn.Conv2d(\n",
    "            in_channels=output_channels,\n",
    "            out_channels=output_channels,\n",
    "            kernel_size=3,\n",
    "            stride=1,\n",
    "            padding=1\n",
    "        )\n",
    "        if use_1x1conv:\n",
    "            # skip connection 的 1x1 卷积，用于改变通道数和降采样，使得最终可以做残差连接\n",
    "            # [1+n-1]//s=n//s\n",
    "            self.conv_sc = nn.Conv2d(\n",
    "                in_channels=input_channels,\n",
    "                out_channels=output_channels,\n",
    "                kernel_size=1,\n",
    "                stride=stride\n",
    "            )\n",
    "        else:\n",
    "            self.conv_sc = None\n",
    "\n",
    "        self.bn1 = nn.BatchNorm2d(output_channels, eps=1e-5, momentum=0.9)\n",
    "        self.bn2 = nn.BatchNorm2d(output_channels, eps=1e-5, momentum=0.9)\n",
    "\n",
    "    def forward(self, inputs):\n",
    "        flow = F.relu(self.bn1(self.conv1(inputs)))  # 卷积->BN->ReLU\n",
    "        flow = self.bn2(self.conv2(flow))  # 卷积->BN\n",
    "        if self.conv_sc:\n",
    "            inputs = self.conv_sc(inputs)  # 1x1卷积，改变通道数和降采样\n",
    "        # 残差连接->ReLU，必须保证flow和inputs的shape相同\n",
    "        return F.relu(flow + inputs)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "fc16467b5391fd55",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.766476Z",
     "start_time": "2025-01-21T14:09:41.761185Z"
    }
   },
   "outputs": [],
   "source": [
    "class ResdiualBlock(nn.Module):\n",
    "    \"\"\"\n",
    "    若干个 Resdiual 模块堆叠在一起，\n",
    "    通常在第一个模块给 skip connection 使用 1x1conv with stride=2\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, input_channels, output_channels, num, is_first=False):\n",
    "        \"\"\"\n",
    "        params filters: 过滤器数目\n",
    "        params num: 堆叠几个 Resdiual 模块\n",
    "        params is_first: 是不是第一个block。 最上面一层 Resdiual 的 stride=1,is_first=False,图像尺寸减半，False图像尺寸不变\n",
    "        \"\"\"\n",
    "        super().__init__()\n",
    "        self.model = nn.Sequential()  # 用于存放 Resdiual 模块\n",
    "        # append() 等价于 add_module()\n",
    "        self.model.append(Resdiual(\n",
    "            input_channels=input_channels,\n",
    "            output_channels=output_channels,\n",
    "            use_1x1conv=not is_first,\n",
    "            stride=1 if is_first else 2\n",
    "        ))  # 第一个 Resdiual 模块，负责通道翻倍,图像的尺寸减半\n",
    "        for _ in range(1, num):\n",
    "            # 堆叠 num 个 Resdiual 模块\n",
    "            self.model.append(Resdiual(\n",
    "                input_channels=output_channels,\n",
    "                output_channels=output_channels,\n",
    "                use_1x1conv=False,\n",
    "                stride=1\n",
    "            ))\n",
    "\n",
    "    def forward(self, inputs):\n",
    "        return self.model(inputs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "8128761d5d6c0e1d",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.773688Z",
     "start_time": "2025-01-21T14:09:41.767479Z"
    }
   },
   "outputs": [],
   "source": [
    "class ResNetCifar10(nn.Module):\n",
    "    def __init__(self, n=3, num_classes=10):\n",
    "        \"\"\"\n",
    "        params units: 预测类别的数目\n",
    "        \"\"\"\n",
    "        super().__init__()\n",
    "        self.model = nn.Sequential(\n",
    "            # conv1\n",
    "            nn.Conv2d(in_channels=3, out_channels=16,\n",
    "                      kernel_size=3, stride=1),\n",
    "            nn.BatchNorm2d(16, momentum=0.9, eps=1e-5),\n",
    "            nn.ReLU(),\n",
    "            # conv2_x\n",
    "            ResdiualBlock(input_channels=16, output_channels=16,\n",
    "                          num=2 * n, is_first=True),\n",
    "            # conv3_x\n",
    "            ResdiualBlock(input_channels=16, output_channels=32,\n",
    "                          num=2 * n),\n",
    "            # conv4_x\n",
    "            ResdiualBlock(input_channels=32, output_channels=64,\n",
    "                          num=2 * n),\n",
    "            # 全局平均池化层\n",
    "            #无论输入图片大小，输出都是1x1，把width和height压缩为1\n",
    "            nn.AdaptiveAvgPool2d((1, 1)),\n",
    "            # 全连接层\n",
    "            nn.Flatten(),  # 64*1*1 -> 64\n",
    "            # 输出层\n",
    "            nn.Linear(in_features=64, out_features=num_classes)\n",
    "        )\n",
    "        self.init_weights()\n",
    "\n",
    "    def init_weights(self):\n",
    "        for m in self.modules():\n",
    "            if isinstance(m, (nn.Linear, nn.Conv2d)):\n",
    "                # Kaiming 初始化（也称为 He 初始化）是为深度神经网络设计的一种初始化方法，特别适用于 ReLU 激活函数。\n",
    "                nn.init.xavier_uniform_(m.weight)\n",
    "                if m.bias is not None:\n",
    "                    nn.init.zeros_(m.bias)\n",
    "    \n",
    "    def forward(self, inputs):\n",
    "        return self.model(inputs)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "bb519b4756d62090",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.777720Z",
     "start_time": "2025-01-21T14:09:41.774691Z"
    }
   },
   "outputs": [],
   "source": [
    "# for key, value in ResNetCifar10(len(class_names)).named_parameters():\n",
    "#     print(f\"{key:^40}paramerters num: {np.prod(value.shape)}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "b294d25e7f6287d0",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.825513Z",
     "start_time": "2025-01-21T14:09:41.779726Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total trainable parameters: 1929546\n"
     ]
    }
   ],
   "source": [
    "total_params = sum(p.numel() for p in ResNetCifar10(len(class_names)).parameters() if p.requires_grad)\n",
    "print(f\"Total trainable parameters: {total_params}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "76ca49d0dca74376",
   "metadata": {},
   "source": [
    "## 模型训练"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "632d31ce33563b0c",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.909287Z",
     "start_time": "2025-01-21T14:09:41.826518Z"
    }
   },
   "outputs": [],
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "\n",
    "@torch.no_grad()  # 装饰器，禁止梯度计算\n",
    "def evaluate(model, data_loader, loss_fct):\n",
    "    loss_list = []\n",
    "    pred_list = []\n",
    "    label_list = []\n",
    "    for datas, labels in data_loader:\n",
    "        datas = datas.to(device)\n",
    "        labels = labels.to(device)\n",
    "\n",
    "        # 前向传播\n",
    "        logits = model(datas)\n",
    "        loss = loss_fct(logits, labels)  # 验证集损失\n",
    "        # tensor.item() 获取tensor的数值，loss是只有一个元素的tensor\n",
    "        loss_list.append(loss.item())\n",
    "\n",
    "        # 预测\n",
    "        preds = logits.argmax(axis=-1)  # 预测类别\n",
    "        pred_list.extend(preds.cpu().numpy().tolist())  # tensor转numpy，再转list\n",
    "        label_list.extend(labels.cpu().numpy().tolist())\n",
    "\n",
    "    acc = accuracy_score(label_list, pred_list)  # 计算准确率\n",
    "    return np.mean(loss_list), acc  # # 返回验证集平均损失和准确率"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "28c2ba49f4ff546",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.915505Z",
     "start_time": "2025-01-21T14:09:41.910290Z"
    }
   },
   "outputs": [],
   "source": [
    "class SaveCheckpointsCallback:\n",
    "    def __init__(self, save_dir, save_step=500, save_best_only=True):\n",
    "        self.save_dir = save_dir  # 保存路径\n",
    "        self.save_step = save_step  # 保存步数\n",
    "        self.save_best_only = save_best_only  # 是否只保存最好的模型\n",
    "        self.best_metric = -1  # 最好的指标，指标不可能为负数，所以初始化为-1\n",
    "        # 创建保存路径\n",
    "        if not os.path.exists(self.save_dir):  # 如果不存在保存路径，则创建\n",
    "            os.makedirs(self.save_dir)\n",
    "\n",
    "    # 对象被调用时：当你将对象像函数一样调用时，Python 会自动调用 __call__ 方法。\n",
    "    # state_dict() 返回模型参数的字典，包括模型参数和优化器参数\n",
    "    # metric 是指标，可以是验证集的准确率，也可以是其他指标\n",
    "    def __call__(self, step, state_dict, metric=None):\n",
    "        if step % self.save_step > 0:\n",
    "            return  # 不是保存步数，则直接返回\n",
    "\n",
    "        if self.save_best_only:\n",
    "            assert metric is not None  # 必须传入metric\n",
    "            if metric >= self.best_metric:  # 如果当前指标大于最好的指标\n",
    "                # save checkpoint\n",
    "                # 保存最好的模型，覆盖之前的模型，不保存step，只保存state_dict，即模型参数，不保存优化器参数\n",
    "                torch.save(state_dict, os.path.join(self.save_dir, \"08_resnet.ckpt\"))\n",
    "                self.best_metric = metric  # 更新最好的指标\n",
    "        else:\n",
    "            # 保存模型\n",
    "            torch.save(state_dict, os.path.join(self.save_dir, f\"{step}.ckpt\"))\n",
    "            # 保存每个step的模型，不覆盖之前的模型，保存step，保存state_dict，即模型参数，不保存优化器参数\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "cd4d61586ad24eb5",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.921575Z",
     "start_time": "2025-01-21T14:09:41.916508Z"
    }
   },
   "outputs": [],
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        self.patience = patience  # 多少个step没有提升就停止训练\n",
    "        self.min_delta = min_delta  # 最小的提升幅度\n",
    "        self.best_metric = -1  # 记录的最好的指标\n",
    "        self.counter = 0  # 计数器，记录连续多少个step没有提升\n",
    "\n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:  # 如果指标提升了\n",
    "            self.best_metric = metric  # 更新最好的指标\n",
    "            self.counter = 0  # 计数器清零\n",
    "        else:\n",
    "            self.counter += 1  # 计数器加一\n",
    "\n",
    "    @property  # 使用@property装饰器，使得 对象.early_stop可以调用，不需要()\n",
    "    def early_stop(self):\n",
    "        # 如果计数器大于等于patience，则返回True，停止训练\n",
    "        return self.counter >= self.patience"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "725cf8a521379801",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.930997Z",
     "start_time": "2025-01-21T14:09:41.922578Z"
    }
   },
   "outputs": [],
   "source": [
    "def training(model,\n",
    "             train_loader,\n",
    "             val_loader,\n",
    "             epoch,\n",
    "             loss_fct,\n",
    "             optimizer,\n",
    "             save_ckpt_callback=None,\n",
    "             early_stop_callback=None,\n",
    "             eval_step=500,\n",
    "             ):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "\n",
    "    global_step = 0  # 全局步数\n",
    "    model.train()  # 训练模式\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            for datas, labels in train_loader:\n",
    "                datas = datas.to(device)\n",
    "                labels = labels.to(device)\n",
    "\n",
    "                # 前向传播\n",
    "                logits = model(datas)\n",
    "                loss = loss_fct(logits, labels)  # 训练集损失\n",
    "                preds = logits.argmax(axis=-1)  # 预测类别\n",
    "\n",
    "                # 反向传播\n",
    "                optimizer.zero_grad()  # 梯度清零\n",
    "                loss.backward()  # 反向传播\n",
    "                optimizer.step()  # 优化器更新参数\n",
    "\n",
    "                # 计算准确率\n",
    "                acc = accuracy_score(labels.cpu().numpy(), preds.cpu().numpy())\n",
    "                loss = loss.cpu().item()\n",
    "\n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss,\n",
    "                    \"acc\": acc,\n",
    "                    \"step\": global_step\n",
    "                })\n",
    "\n",
    "                # 评估\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval()  # 评估模式\n",
    "                    # 验证集损失和准确率\n",
    "                    val_loss, val_acc = evaluate(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss,\n",
    "                        \"acc\": val_acc,\n",
    "                        \"step\": global_step\n",
    "                    })\n",
    "                    model.train()  # 训练模式\n",
    "\n",
    "                    # 2. 保存模型权重 save model checkpoint\n",
    "                    if save_ckpt_callback is not None:\n",
    "                        # model.state_dict() 返回模型参数的字典，包括模型参数和优化器参数\n",
    "                        save_ckpt_callback(global_step, model.state_dict(), val_acc)\n",
    "                        # 保存最好的模型，覆盖之前的模型，保存step，保存state_dict,通过metric判断是否保存最好的模型\n",
    "\n",
    "                    # 3. 早停 early stopping\n",
    "                    if early_stop_callback is not None:\n",
    "                        # 验证集准确率不再提升，则停止训练\n",
    "                        early_stop_callback(val_acc)\n",
    "                        # 验证集准确率不再提升，则停止训练\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict  # 早停，返回记录字典 record_dict\n",
    "\n",
    "                # 更新进度条和全局步数\n",
    "                pbar.update(1)  # 更新进度条\n",
    "                global_step += 1  # 全局步数加一\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "\n",
    "    return record_dict  # 训练结束，返回记录字典 record_dict\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "22bf1e82c9715088",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.979444Z",
     "start_time": "2025-01-21T14:09:41.931999Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "epoch = 100\n",
    "\n",
    "model = ResNetCifar10(len(class_names))  # 定义模型\n",
    "\n",
    "# 1. 定义损失函数 采用MSE损失\n",
    "loss_fct = nn.CrossEntropyLoss()\n",
    "\n",
    "# 2. 定义优化器 采用 adam\n",
    "optimizer = torch.optim.Adam(model.parameters(), lr=0.001)\n",
    "\n",
    "# 3.save model checkpoint\n",
    "if not os.path.exists(\"checkpoints\"):\n",
    "    os.makedirs(\"checkpoints\")\n",
    "save_ckpt_callback = SaveCheckpointsCallback(save_dir=\"checkpoints\", save_step=len(train_loader), save_best_only=True)\n",
    "\n",
    "# 4. early stopping\n",
    "early_stop_callback = EarlyStopCallback(patience=5, min_delta=0.01)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "5335f62fe5e4977f",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:10:19.118493Z",
     "start_time": "2025-01-21T14:09:41.980446Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      " 23%|██▎       | 16192/70400 [19:38<1:05:45, 13.74it/s, epoch=22]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Early stop at epoch 23 / global_step 16192\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "model = model.to(device)  # 将模型移到GPU上\n",
    "\n",
    "# 训练过程\n",
    "record_dict = training(\n",
    "    model,\n",
    "    train_loader,\n",
    "    eval_loader,\n",
    "    epoch,\n",
    "    loss_fct,\n",
    "    optimizer,\n",
    "    save_ckpt_callback=save_ckpt_callback,\n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=len(train_loader)\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "1a790e5f4635d5e8",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:10:19.120498Z",
     "start_time": "2025-01-21T14:10:19.119497Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "image/png": "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64tdrX7tkMvYmIoOEleRibMkJ9BDEQl55Q4nA+d9EpBrb753MlG2xv0Ke/TRR9m+fTuDBw/m3nvvBeDHH38E4Pbbb+fhhx+mV69eJCYmsn//fqZOncp9991HeHg4L730EtOmTWPbtm107969wWvcc889PPjggzz00EM8/vjjXH311ezdu5ekpCTACIZmzJjBvHnz6j3+T3/6E//73/948cUX6dGjBw8++CCTJ09mx44dJCUlceedd7J582Y++ugjOnbsyI4dOygvLwfgscce47333uM///kP3bt3Z//+/ezfv7/Rr6WIiL+s33eE57/aA8DVY3rQJSEysAOSVvfU8h0cLq3kUGFFq1875AIbbNVNAx2lgR2HiASN+Ph4bDYbUVFR3u70W7canybee++9TJw40btvUlISw4YN8z7+61//yttvv81777133AaQM2bM4KqrrgLg/vvv57HHHmPNmjWcd955APTu3ZuOHTvWe2xpaSlPPfUUL7zwAlOmTAHg2WefJSMjg+eee44//vGP7Nu3j5NPPpmRI0cCRqDksW/fPvr27cvpp5+OyWSiR48evr5EIiJ+8c53B73fr92Tz0XDuwZwNNLa1u87wupd+VgtJn51Rs9Wv37IBTZuqyewKQvsQETagUirhc33Tg7Ytf3BEyh4lJSUcPfdd/Phhx9y6NAhqqqqKC8vZ9++fcc9z9ChQ73fR0dHExcXR07O0czxsmXLAHC5XHWO3blzJw6Hg3Hjxnm3Wa1WRo8ezZYtWwC44YYb+PnPf8769euZNGkSF198MWPHjgWMoGrixIn079+f8847jwsuuIBJkyb5+EqIiDSPw+niw02HvI+/3XNEgU0789TynQBccnJXOse3frYu5AIbFNiItBqTydTo6WDB6tjqZn/4wx/IyMjg4Ycfpk+fPkRGRnLppZdSWVl53PNYrdZaj00mU71BTFNNmTKFvXv3smTJEjIyMjj33HO56aabePjhhznllFPYvXs3H330EZ9++imXX345EyZM4M033/Tb9UVETuTLHXnklx79Xbl2T34ARyOtbXt2MRmbszGZ4Ddn9Q7IGEKueMDRwKY8sOMQkaBis9lwOp0n3O+rr75ixowZXHLJJQwZMoTU1FT27NnTomPr3bs3NpuNr776yrvN4XCwdu1aBg4c6N2WnJzM9OnTeeWVV1i4cCHPPPOM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",
      "text/plain": [
       "<Figure size 1000x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "def plot_record_curves(record_dict, sample_step=500):\n",
    "    # .set_index(\"step\") 将 step 列设置为 DataFrame 的索引\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    last_step = train_df.index[-1]  # 最后一步的步数\n",
    "\n",
    "    # print(train_df)\n",
    "    # print(val_df)\n",
    "\n",
    "    # 画图 \n",
    "    fig_num = len(train_df.columns)  # 画两张图,分别是损失和准确率\n",
    "\n",
    "    # plt.subplots：用于创建一个包含多个子图的图形窗口。\n",
    "    # 1：表示子图的行数为 1。\n",
    "    # fig_num：表示子图的列数，即子图的数量。\n",
    "    # figsize=(5 * fig_num, 5)：设置整个图形窗口的大小，宽度为 5 * fig_num，高度为 5。\n",
    "    # fig：返回的图形对象（Figure），用于操作整个图形窗口。\n",
    "    # axs：返回的子图对象（Axes 或 Axes 数组），用于操作每个子图。\n",
    "    fig, axs = plt.subplots(1, fig_num, figsize=(5 * fig_num, 5))\n",
    "    for idx, item in enumerate(train_df.columns):\n",
    "        # train_df.index 是 x 轴数据（通常是 step）。\n",
    "        # train_df[item] 是 y 轴数据（当前指标的值）。\n",
    "        axs[idx].plot(train_df.index, train_df[item], label=\"train:\" + item)\n",
    "        # val_df.index 是 x 轴数据。\n",
    "        # val_df[item] 是 y 轴数据。\n",
    "        axs[idx].plot(val_df.index, val_df[item], label=\"val:\" + item)\n",
    "        axs[idx].grid()  # 显示网格\n",
    "        axs[idx].legend()  # 显示图例\n",
    "        axs[idx].set_xticks(range(0, train_df.index[-1] + 1, 5000))  # 设置x轴刻度\n",
    "        axs[idx].set_xticklabels(map(lambda x: f\"{x // 1000}k\", range(0, last_step + 1, 5000)))  # 设置x轴标签\n",
    "        axs[idx].set_xlabel(\"step\")\n",
    "\n",
    "    plt.show()\n",
    "\n",
    "\n",
    "plot_record_curves(record_dict)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5d9729d15a0d01e8",
   "metadata": {},
   "source": [
    "## 评估"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "3195e10a60c29806",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:10:19.121498Z",
     "start_time": "2025-01-21T14:10:19.121498Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Test loss: 0.4828, Test acc: 0.8386\n"
     ]
    }
   ],
   "source": [
    "# 加载最好的模型\n",
    "# torch.load：加载保存的模型权重或整个模型。\n",
    "# \"checkpoints/best.ckpt\"：模型权重文件路径。\n",
    "# weights_only=True：仅加载模型的权重，而不是整个模型（包括结构和参数）。这是 PyTorch 2.1 引入的新特性，用于增强安全性。\n",
    "# map_location=device：将模型加载到当前设备（GPU或CPU）。\n",
    "model.load_state_dict(torch.load(\"checkpoints/08_resnet.ckpt\", weights_only=True, map_location=device))  # 加载最好的模型\n",
    "\n",
    "model.eval()  # 评估模式\n",
    "loss, acc = evaluate(model, eval_loader, loss_fct)\n",
    "print(f\"Test loss: {loss:.4f}, Test acc: {acc:.4f}\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
